/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package abricots.net;

import abricots.net.common.NetUtils;
import abricots.MyLog;
import abricots.entity.Entity;
import abricots.entity.ServerEntity;
import abricots.entity.state.EntityState;
import abricots.game.EntityFactory;
import abricots.game.GameConstants;
import abricots.game.GameEnv;
import abricots.game.WorldStateManager;
import abricots.net.common.GameMessage.CommandsMessage;
import abricots.net.common.GameMessage.EntityMessage;
import abricots.net.common.GameMessage.WorldDiffMessage;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.Server;
import com.esotericsoftware.minlog.Log;
import java.io.IOException;
import java.util.Set;

/**
 *
 * @author charly
 */
public class GameServer extends Listener {
   private final MyLog log = new MyLog(getClass().getSimpleName(), Log.LEVEL_TRACE);
   private Server kryoServer;
   private GameEnv env;
   private ServerEntity entity;
   private ServerMessageDispatcher dispatcher;
   private ServerConnectionsHandler connectionsHandler;

   public GameServer() {
      env = new GameEnv();
      env.init();
      dispatcher = new ServerMessageDispatcher(this, env);
      connectionsHandler = new ServerConnectionsHandler();
   }

   public void loop() throws InterruptedException, IOException {
      while (true) {
         log.trace("===========NEW FRAME");
         prepareUpdate();
         simulate();
         buildAndSendDiffs();
         buildAndSendKeyFrames();
         cleanUpUpdate();
         log.trace("===========END FRAME");
         sleepToNextFrame();
      }
   }

   public void startServer() {
      try {
         kryoServer = new Server() {
            @Override
            protected Connection newConnection() {
               return new ServerConnection(dispatcher);
            }
         };
         NetUtils.registerClasses(kryoServer);
         kryoServer.start();
         kryoServer.bind(NetUtils.TCP_PORT, 4002);
         kryoServer.addListener(this);
         env.getTimeManager().setGameStartTime(System.currentTimeMillis());

         entity = (ServerEntity) EntityFactory.build(GameConstants.ENTITY_TYPE_ID_SERVER);
         entity.register(env);
         Log.info("SERVER ENTITY ID: " + entity.getId());
         env.getEntitiesManager().validateEntity(entity.getId(), entity.getId());
      } catch (IOException ex) {
         log.error("Failed to start server", ex);
      }
      new Thread() {
         @Override
         public void run() {
            try {
               loop();
            } catch (IOException ex) {
               log.error("Server loop failed", ex);
            } catch (InterruptedException ex) {
               log.error("server loop interupted", ex);
            }
         }
      }.start();
   }

   private void prepareUpdate() {
      env.getTimeManager().newUpdate();
      for (int id : connectionsHandler.getConnectionsIds()) {
         ServerConnection connection = connectionsHandler.getConnection(id);
         addNewEntities(connection);
         addNewCommands(connection);
      }
      log.trace("Simulation times: " + env.getTimeManager().getTimes().toString());
      // update timemanager
      // clear all
   }

   private void addNewEntities(ServerConnection connection) {
      // Add new entities
      Set<EntityMessage> newEntities = connection.getNewEntities();
      connection.clearNewEntities();
      for (EntityMessage entityMessage : newEntities) {
         int lastValidatedLocalId = connection.getLastValidatedEntityLocalId();
         if (entityMessage.localEntityId < 0 || entityMessage.localEntityId < 0) {
            log.debug("Unknown entity message: " + entityMessage.toString());
            continue;
         }
         if (entityMessage.localEntityId <= lastValidatedLocalId) {
            continue;
         }
         Entity newEntity = EntityFactory.build(entityMessage.entityTypeId);
         newEntity.register(env);
         env.getEntitiesManager().validateEntity(newEntity.getId(), newEntity.getId());
         connection.setLastValidatedEntityLocalId(entityMessage.localEntityId);
         EntityMessage answer = new EntityMessage(entityMessage.localEntityId, newEntity.getId(), newEntity.getEntityTypeId());
         connection.send(answer);
         connection.setEntityId(newEntity.getId());
      }
   }

   private void addNewCommands(ServerConnection connection) {
      Entity cmdEntity = env.getEntitiesManager().getEntity(connection.getEntityId());
      Set<CommandsMessage> messages = connection.getAndClearCommandsMessages();
      long lastSimulatedTime = -1;
      boolean newCmdsReceived = false;
      if (cmdEntity != null) {
         lastSimulatedTime = cmdEntity.getCommandManager().getLastCommandTime();
      }
      for (CommandsMessage commandsMessage : messages) {
         if (commandsMessage.commands == null || commandsMessage.commands.isEmpty()) {
            continue;
         }
         if (cmdEntity == null) {
            log.debug("Received commands for connection " + connection.getID() + " with no registered entity");
            continue;
         }
         newCmdsReceived = true;
         cmdEntity.getCommandManager().addCommands(commandsMessage.commands);
      }
      long lastReceivedCommandTime = connection.getLastReceivedCommandTime();
      env.getTimeManager().addIntermediateTime(lastReceivedCommandTime);
      if (newCmdsReceived && lastSimulatedTime >= 0 && lastSimulatedTime < lastReceivedCommandTime) {
         final long lastAcked = connection.getAckedFramesManager().getLastAckedTime();
         log.trace("Last command present before adding new ones for " + lastSimulatedTime);
         log.trace("Last acked frame for " + lastAcked);
         long lastValid = Math.max(lastSimulatedTime, lastAcked);
         log.trace("Clearing all after " + lastValid);
         // Clear all states that need to be resimulated due to new commands.
         // Clear them for all entities
         cmdEntity.getStateManager().setLastValid(lastValid);
         env.getEntitiesManager().cleanUp(-1, lastValid);
      }
   }

   private void simulate() {
      // update & simulate
      env.getEntitiesManager().getRootEntity().simulate();
   }

   private void buildAndSendDiffs() {
      long frameTime = env.getTimeManager().getLastUpdateTime();
      for (int id : connectionsHandler.getConnectionsIds()) {
         ServerConnection connection = connectionsHandler.getConnection(id);
         long lastAckedTime = connection.getAckedFramesManager().getLastAckedTime();
         long lastReceivedCmdsTime = env.getWorldStateManager().getLastStateUpTo(connection.getLastReceivedCommandTime());
         if (lastReceivedCmdsTime < 0) {
            continue;
         }
         long diffToTime = Math.min(frameTime, lastReceivedCmdsTime);
         WorldStateManager stateManager = env.getWorldStateManager();
         WorldDiffMessage diffMessage = stateManager.buildDiff(lastAckedTime, diffToTime);
         int messageId = connection.send(diffMessage);
         connection.getAckedFramesManager().newSentState(messageId, diffMessage.toTime);
         log.trace("Sent diff to " + id + " : " + diffMessage.toString());
         for (EntityState state : diffMessage.modifiedStates) {
            log.trace(" Modified: " + state);
         }
      }
   }

   private void buildAndSendKeyFrames() {
      long keyFrameTime = env.getTimeManager().getLastKeyFrame();
      if (keyFrameTime < 0) {
         return;
      }
      sendKeyFrame(keyFrameTime);
   }

   private void cleanUpUpdate() {
      long lastNeeded = env.getTimeManager().getPreviousUpdateTime();
      if (connectionsHandler.getOldestAckedTime() >= 0) {
         lastNeeded = connectionsHandler.getOldestAckedTime();
      }
      env.getEntitiesManager().cleanUp(lastNeeded, -1);
      env.getTimeManager().clearTimesBefore(lastNeeded);
      log.trace("Cleared before " + lastNeeded);

      for (int conectionId : connectionsHandler.getConnectionsIds()) {
         ServerConnection connection = connectionsHandler.getConnection(conectionId);
         int entityId = connection.getEntityId();
         if (entityId < 0) {
            continue;
         }
         Entity connectionEntity = env.getEntitiesManager().getEntity(entityId);
         if (connectionEntity == null) {
            continue;
         }
         long lastReceivedCmd = connection.getLastReceivedCommandTime();
         if (lastReceivedCmd >= 0) {
            connectionEntity.getStateManager().setLastValid(lastReceivedCmd);
         }
      }
   }

   private void sleepToNextFrame() {
      long nextFrame = env.getTimeManager().getNextKeyFrame();
      long toSleep = nextFrame - env.getTimeManager().getGameTime();
      if (toSleep < 0) {
         toSleep = 0;
         log.warn("Server took too long!");
         log.warn("Next frame is " + nextFrame);
         log.warn("and we are at " + env.getTimeManager().getGameTime());
      }
      try {
         Thread.sleep(toSleep);
      } catch (InterruptedException ex) {
         log.error("Server sleep was interrupted!", ex);
      }
   }

   private void sendKeyFrame(long nextKeyFrameTime) {
   }

   @Override
   public void connected(Connection connection) {
      super.connected(connection);
      connectionsHandler.addConnection((ServerConnection) connection);
   }

   @Override
   public void disconnected(Connection connection) {
      super.disconnected(connection);
      ServerConnection serverConnection = (ServerConnection) connection;
      int id = serverConnection.getEntityId();
      if (id >= 0) {
         Entity connectionEntity = env.getEntitiesManager().getEntity(id);
         if (connectionEntity != null) {
            connectionEntity.unregister();
         }
      }
      connectionsHandler.removeConnection((ServerConnection) connection);
   }

   @Override
   public void received(Connection connection, Object object) {
      //Dispatched by the connection
   }

//   public ServerConnectionsHandler getConnectionsHandler() {
//      return connectionsHandler;
//   }
   public GameEnv getEnv() {
      return env;
   }
}
